﻿/**
 * Copyright(c) Live2D Inc. All rights reserved.
 *
 * Use of this source code is governed by the Live2D Open Software license
 * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html.
 */

#include "CubismMotion.hpp"
#include <float.h>
#include "CubismFramework.hpp"
#include "CubismMotionInternal.hpp"
#include "CubismMotionJson.hpp"
#include "CubismMotionQueueManager.hpp"
#include "CubismMotionQueueEntry.hpp"
#include "Math/CubismMath.hpp"
#include "Type/csmVector.hpp"
#include "Id/CubismIdManager.hpp"

namespace Live2D {
    namespace Cubism {
        namespace Framework {

            namespace {

                const csmChar *EffectNameEyeBlink = "EyeBlink";
                const csmChar *EffectNameLipSync = "LipSync";
                const csmChar *TargetNameModel = "Model";
                const csmChar *TargetNameParameter = "Parameter";
                const csmChar *TargetNamePartOpacity = "PartOpacity";

                CubismMotionPoint LerpPoints(const CubismMotionPoint a, const CubismMotionPoint b,
                                             const csmFloat32 t) {
                    CubismMotionPoint result;

                    result.Time = a.Time + ((b.Time - a.Time) * t);
                    result.Value = a.Value + ((b.Value - a.Value) * t);

                    return result;
                }

                csmFloat32 LinearEvaluate(const CubismMotionPoint *points, const csmFloat32 time) {
                    csmFloat32 t = (time - points[0].Time) / (points[1].Time - points[0].Time);

                    if (t < 0.0f) {
                        t = 0.0f;
                    }

                    return points[0].Value + ((points[1].Value - points[0].Value) * t);
                }

                csmFloat32 BezierEvaluate(const CubismMotionPoint *points, const csmFloat32 time) {
                    csmFloat32 t = (time - points[0].Time) / (points[3].Time - points[0].Time);

                    if (t < 0.0f) {
                        t = 0.0f;
                    }

                    const CubismMotionPoint p01 = LerpPoints(points[0], points[1], t);
                    const CubismMotionPoint p12 = LerpPoints(points[1], points[2], t);
                    const CubismMotionPoint p23 = LerpPoints(points[2], points[3], t);

                    const CubismMotionPoint p012 = LerpPoints(p01, p12, t);
                    const CubismMotionPoint p123 = LerpPoints(p12, p23, t);

                    return LerpPoints(p012, p123, t).Value;
                }

                csmFloat32 SteppedEvaluate(const CubismMotionPoint *points, const csmFloat32 time) {
                    return points[0].Value;
                }

                csmFloat32
                InverseSteppedEvaluate(const CubismMotionPoint *points, const csmFloat32 time) {
                    return points[1].Value;
                }

                csmFloat32 EvaluateCurve(const CubismMotionData *motionData, const csmInt32 index,
                                         csmFloat32 time) {
                    // Find segment to evaluate.
                    const CubismMotionCurve &curve = motionData->Curves[index];

                    csmInt32 target = -1;
                    const csmInt32 totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount;
                    csmInt32 pointPosition = 0;
                    for (csmInt32 i = curve.BaseSegmentIndex; i < totalSegmentCount; ++i) {
                        // Get first point of next segment.
                        pointPosition = motionData->Segments[i].BasePointIndex
                                        + (motionData->Segments[i].SegmentType ==
                                           CubismMotionSegmentType_Bezier
                                           ? 3
                                           : 1);


                        // Break if time lies within current segment.
                        if (motionData->Points[pointPosition].Time > time) {
                            target = i;
                            break;
                        }
                    }


                    if (target == -1) {
                        return motionData->Points[pointPosition].Value;
                    }


                    const CubismMotionSegment &segment = motionData->Segments[target];

                    return segment.Evaluate(&motionData->Points[segment.BasePointIndex], time);
                }

            }

            CubismMotion::CubismMotion()
                    : _sourceFrameRate(30.0f), _loopDurationSeconds(-1.0f),
                      _isLoop(false)                // trueから false へデフォルトを変更
                    , _isLoopFadeIn(true)           // ループ時にフェードインが有効かどうかのフラグ
                    , _lastWeight(0.0f), _motionData(NULL), _modelCurveIdEyeBlink(NULL),
                      _modelCurveIdLipSync(NULL) {}

            CubismMotion::~CubismMotion() {
                CSM_DELETE(_motionData);
            }

            CubismMotion *CubismMotion::Create(const csmByte *buffer, csmSizeInt size,
                                               FinishedMotionCallback onFinishedMotionHandler) {
                CubismMotion *ret = CSM_NEW CubismMotion();

                ret->Parse(buffer, size);
                ret->_sourceFrameRate = ret->_motionData->Fps;
                ret->_loopDurationSeconds = ret->_motionData->Duration;
                ret->_onFinishedMotion = onFinishedMotionHandler;

                // NOTE: Editorではループありのモーション書き出しは非対応
                // ret->_loop = (ret->_motionData->Loop > 0);

                return ret;
            }

            csmFloat32 CubismMotion::GetDuration() {
                return _isLoop ? -1.0f : _loopDurationSeconds;
            }

            void CubismMotion::DoUpdateParameters(CubismModel *model, csmFloat32 userTimeSeconds,
                                                  csmFloat32 fadeWeight,
                                                  CubismMotionQueueEntry *motionQueueEntry) {
                if (_modelCurveIdEyeBlink == NULL) {
                    _modelCurveIdEyeBlink = CubismFramework::GetIdManager()->GetId(
                            EffectNameEyeBlink);
                }

                if (_modelCurveIdLipSync == NULL) {
                    _modelCurveIdLipSync = CubismFramework::GetIdManager()->GetId(
                            EffectNameLipSync);
                }

                csmFloat32 timeOffsetSeconds = userTimeSeconds - motionQueueEntry->GetStartTime();

                if (timeOffsetSeconds < 0.0f) {
                    timeOffsetSeconds = 0.0f; // エラー回避
                }

                csmFloat32 lipSyncValue = FLT_MAX;
                csmFloat32 eyeBlinkValue = FLT_MAX;

                //まばたき、リップシンクのうちモーションの適用を検出するためのビット（maxFlagCount個まで
                const csmInt32 MaxTargetSize = 64;
                csmUint64 lipSyncFlags = 0ULL;
                csmUint64 eyeBlinkFlags = 0ULL;

                //瞬き、リップシンクのターゲット数が上限を超えている場合
                if (_eyeBlinkParameterIds.GetSize() > MaxTargetSize) {
                    CubismLogDebug("too many eye blink targets : %d",
                                   _eyeBlinkParameterIds.GetSize());
                }
                if (_lipSyncParameterIds.GetSize() > MaxTargetSize) {
                    CubismLogDebug("too many lip sync targets : %d",
                                   _lipSyncParameterIds.GetSize());
                }

                const csmFloat32 tmpFadeIn = (_fadeInSeconds <= 0.0f)
                                             ? 1.0f
                                             : CubismMath::GetEasingSine(
                                (userTimeSeconds - motionQueueEntry->GetFadeInStartTime()) /
                                _fadeInSeconds);

                const csmFloat32 tmpFadeOut = (_fadeOutSeconds <= 0.0f ||
                                               motionQueueEntry->GetEndTime() < 0.0f)
                                              ? 1.0f
                                              : CubismMath::GetEasingSine(
                                (motionQueueEntry->GetEndTime() - userTimeSeconds) /
                                _fadeOutSeconds);

                csmFloat32 value;
                csmInt32 c, parameterIndex;

                // 'Repeat' time as necessary.
                csmFloat32 time = timeOffsetSeconds;

                if (_isLoop) {
                    while (time > _motionData->Duration) {
                        time -= _motionData->Duration;
                    }
                }

                csmVector<CubismMotionCurve> &curves = _motionData->Curves;

                // Evaluate model curves.
                for (c = 0; c < _motionData->CurveCount &&
                            curves[c].Type == CubismMotionCurveTarget_Model; ++c) {
                    // Evaluate curve and call handler.
                    value = EvaluateCurve(_motionData, c, time);

                    if (curves[c].Id == _modelCurveIdEyeBlink) {
                        eyeBlinkValue = value;
                    } else if (curves[c].Id == _modelCurveIdLipSync) {
                        lipSyncValue = value;
                    }
                }

                csmInt32 parameterMotionCurveCount = 0;

                for (; c < _motionData->CurveCount &&
                       curves[c].Type == CubismMotionCurveTarget_Parameter; ++c) {
                    parameterMotionCurveCount++;

                    // Find parameter index.
                    parameterIndex = model->GetParameterIndex(curves[c].Id);

                    // Skip curve evaluation if no value in sink.
                    if (parameterIndex == -1) {
                        continue;
                    }

                    const csmFloat32 sourceValue = model->GetParameterValue(parameterIndex);

                    // Evaluate curve and apply value.
                    value = EvaluateCurve(_motionData, c, time);

                    if (eyeBlinkValue != FLT_MAX) {
                        for (csmUint32 i = 0;
                             i < _eyeBlinkParameterIds.GetSize() && i < MaxTargetSize; ++i) {
                            if (_eyeBlinkParameterIds[i] == curves[c].Id) {
                                value *= eyeBlinkValue;
                                eyeBlinkFlags |= 1ULL << i;
                                break;
                            }
                        }
                    }

                    if (lipSyncValue != FLT_MAX) {
                        for (csmUint32 i = 0;
                             i < _lipSyncParameterIds.GetSize() && i < MaxTargetSize; ++i) {
                            if (_lipSyncParameterIds[i] == curves[c].Id) {
                                value += lipSyncValue;
                                lipSyncFlags |= 1ULL << i;
                                break;
                            }
                        }
                    }

                    csmFloat32 v;
                    // パラメータごとのフェード
                    if (curves[c].FadeInTime < 0.0f && curves[c].FadeOutTime < 0.0f) {
                        //モーションのフェードを適用
                        v = sourceValue + (value - sourceValue) * fadeWeight;
                    } else {
                        // パラメータに対してフェードインかフェードアウトが設定してある場合はそちらを適用
                        csmFloat32 fin;
                        csmFloat32 fout;

                        if (curves[c].FadeInTime < 0.0f) {
                            fin = tmpFadeIn;
                        } else {
                            fin = curves[c].FadeInTime == 0.0f
                                  ? 1.0f
                                  : CubismMath::GetEasingSine((userTimeSeconds -
                                                               motionQueueEntry->GetFadeInStartTime()) /
                                                              curves[c].FadeInTime);
                        }

                        if (curves[c].FadeOutTime < 0.0f) {
                            fout = tmpFadeOut;
                        } else {
                            fout = (curves[c].FadeOutTime == 0.0f ||
                                    motionQueueEntry->GetEndTime() < 0.0f)
                                   ? 1.0f
                                   : CubismMath::GetEasingSine(
                                            (motionQueueEntry->GetEndTime() - userTimeSeconds) /
                                            curves[c].FadeOutTime);
                        }

                        const csmFloat32 paramWeight = _weight * fin * fout;

                        // パラメータごとのフェードを適用
                        v = sourceValue + (value - sourceValue) * paramWeight;
                    }

                    model->SetParameterValue(parameterIndex, v);
                }

                {
                    if (eyeBlinkValue != FLT_MAX) {
                        for (csmUint32 i = 0;
                             i < _eyeBlinkParameterIds.GetSize() && i < MaxTargetSize; ++i) {
                            const csmFloat32 sourceValue = model->GetParameterValue(
                                    _eyeBlinkParameterIds[i]);
                            //モーションでの上書きがあった時にはまばたきは適用しない
                            if ((eyeBlinkFlags >> i) & 0x01) {
                                continue;
                            }

                            const csmFloat32 v =
                                    sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight;

                            model->SetParameterValue(_eyeBlinkParameterIds[i], v);
                        }
                    }

                    if (lipSyncValue != FLT_MAX) {
                        for (csmUint32 i = 0;
                             i < _lipSyncParameterIds.GetSize() && i < MaxTargetSize; ++i) {
                            const csmFloat32 sourceValue = model->GetParameterValue(
                                    _lipSyncParameterIds[i]);
                            //モーションでの上書きがあった時にはリップシンクは適用しない
                            if ((lipSyncFlags >> i) & 0x01) {
                                continue;
                            }

                            const csmFloat32 v =
                                    sourceValue + (lipSyncValue - sourceValue) * fadeWeight;

                            model->SetParameterValue(_lipSyncParameterIds[i], v);
                        }
                    }
                }

                for (; c < _motionData->CurveCount &&
                       curves[c].Type == CubismMotionCurveTarget_PartOpacity; ++c) {
                    // Find parameter index.
                    parameterIndex = model->GetParameterIndex(curves[c].Id);

                    // Skip curve evaluation if no value in sink.
                    if (parameterIndex == -1) {
                        continue;
                    }

                    // Evaluate curve and apply value.
                    value = EvaluateCurve(_motionData, c, time);

                    model->SetParameterValue(parameterIndex, value);
                }

                if (timeOffsetSeconds >= _motionData->Duration) {
                    if (_isLoop) {
                        motionQueueEntry->SetStartTime(userTimeSeconds); //最初の状態へ
                        if (_isLoopFadeIn) {
                            //ループ中でループ用フェードインが有効のときは、フェードイン設定し直し
                            motionQueueEntry->SetFadeInStartTime(userTimeSeconds);
                        }
                    } else {
                        if (this->_onFinishedMotion != NULL) {
                            this->_onFinishedMotion(this);
                        }

                        motionQueueEntry->IsFinished(true);
                    }
                }

                _lastWeight = fadeWeight;
            }

            void CubismMotion::Parse(const csmByte *motionJson, const csmSizeInt size) {
                _motionData = CSM_NEW CubismMotionData;

                CubismMotionJson *json = CSM_NEW CubismMotionJson(motionJson, size);

                _motionData->Duration = json->GetMotionDuration();
                _motionData->Loop = json->IsMotionLoop();
                _motionData->CurveCount = json->GetMotionCurveCount();
                _motionData->Fps = json->GetMotionFps();
                _motionData->EventCount = json->GetEventCount();

                if (json->IsExistMotionFadeInTime()) {
                    _fadeInSeconds = (json->GetMotionFadeInTime() < 0.0f)
                                     ? 1.0f
                                     : json->GetMotionFadeInTime();
                } else {
                    _fadeInSeconds = 1.0f;
                }

                if (json->IsExistMotionFadeOutTime()) {
                    _fadeOutSeconds = (json->GetMotionFadeOutTime() < 0.0f)
                                      ? 1.0f
                                      : json->GetMotionFadeOutTime();
                } else {
                    _fadeOutSeconds = 1.0f;
                }

                _motionData->Curves.UpdateSize(_motionData->CurveCount, CubismMotionCurve(), true);
                _motionData->Segments.UpdateSize(json->GetMotionTotalSegmentCount(),
                                                 CubismMotionSegment(), true);
                _motionData->Points.UpdateSize(json->GetMotionTotalPointCount(),
                                               CubismMotionPoint(), true);
                _motionData->Events.UpdateSize(_motionData->EventCount, CubismMotionEvent(), true);

                csmInt32 totalPointCount = 0;
                csmInt32 totalSegmentCount = 0;

                // Curves
                for (csmInt32 curveCount = 0; curveCount < _motionData->CurveCount; ++curveCount) {
                    if (strcmp(json->GetMotionCurveTarget(curveCount), TargetNameModel) == 0) {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Model;
                    } else if (
                            strcmp(json->GetMotionCurveTarget(curveCount), TargetNameParameter) ==
                            0) {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_Parameter;
                    } else if (
                            strcmp(json->GetMotionCurveTarget(curveCount), TargetNamePartOpacity) ==
                            0) {
                        _motionData->Curves[curveCount].Type = CubismMotionCurveTarget_PartOpacity;
                    }

                    _motionData->Curves[curveCount].Id = json->GetMotionCurveId(curveCount);

                    _motionData->Curves[curveCount].BaseSegmentIndex = totalSegmentCount;

                    _motionData->Curves[curveCount].FadeInTime =
                            (json->IsExistMotionCurveFadeInTime(curveCount))
                            ? json->GetMotionCurveFadeInTime(curveCount)
                            : -1.0f;
                    _motionData->Curves[curveCount].FadeOutTime =
                            (json->IsExistMotionCurveFadeOutTime(curveCount))
                            ? json->GetMotionCurveFadeOutTime(curveCount)
                            : -1.0f;

                    // Segments
                    for (csmInt32 segmentPosition = 0;
                         segmentPosition < json->GetMotionCurveSegmentCount(curveCount);) {
                        if (segmentPosition == 0) {
                            _motionData->Segments[totalSegmentCount].BasePointIndex = totalPointCount;

                            _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(
                                    curveCount, segmentPosition);
                            _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(
                                    curveCount, segmentPosition + 1);

                            totalPointCount += 1;
                            segmentPosition += 2;
                        } else {
                            _motionData->Segments[totalSegmentCount].BasePointIndex =
                                    totalPointCount - 1;
                        }

                        const csmInt32 segment = static_cast<csmInt32>(json->GetMotionCurveSegment(
                                curveCount, segmentPosition));

                        switch (segment) {
                            case CubismMotionSegmentType_Linear: {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Linear;
                                _motionData->Segments[totalSegmentCount].Evaluate = LinearEvaluate;

                                _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            case CubismMotionSegmentType_Bezier: {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Bezier;
                                _motionData->Segments[totalSegmentCount].Evaluate = BezierEvaluate;

                                _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 2));

                                _motionData->Points[totalPointCount +
                                                    1].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 3));
                                _motionData->Points[totalPointCount +
                                                    1].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 4));

                                _motionData->Points[totalPointCount +
                                                    2].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 5));
                                _motionData->Points[totalPointCount +
                                                    2].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 6));

                                totalPointCount += 3;
                                segmentPosition += 7;

                                break;
                            }
                            case CubismMotionSegmentType_Stepped: {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_Stepped;
                                _motionData->Segments[totalSegmentCount].Evaluate = SteppedEvaluate;

                                _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            case CubismMotionSegmentType_InverseStepped: {
                                _motionData->Segments[totalSegmentCount].SegmentType = CubismMotionSegmentType_InverseStepped;
                                _motionData->Segments[totalSegmentCount].Evaluate = InverseSteppedEvaluate;

                                _motionData->Points[totalPointCount].Time = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 1));
                                _motionData->Points[totalPointCount].Value = json->GetMotionCurveSegment(
                                        curveCount, (segmentPosition + 2));

                                totalPointCount += 1;
                                segmentPosition += 3;

                                break;
                            }
                            default: {
                                CSM_ASSERT(0);
                                break;
                            }
                        }

                        ++_motionData->Curves[curveCount].SegmentCount;
                        ++totalSegmentCount;
                    }
                }


                for (csmInt32 userdatacount = 0;
                     userdatacount < json->GetEventCount(); ++userdatacount) {
                    _motionData->Events[userdatacount].FireTime = json->GetEventTime(userdatacount);
                    _motionData->Events[userdatacount].Value = json->GetEventValue(userdatacount);
                }

                CSM_DELETE(json);
            }

            void
            CubismMotion::SetParameterFadeInTime(CubismIdHandle parameterId, csmFloat32 value) {
                csmVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) {
                    if (parameterId == curves[i].Id) {
                        curves[i].FadeInTime = value;
                        return;
                    }
                }
            }

            void
            CubismMotion::SetParameterFadeOutTime(CubismIdHandle parameterId, csmFloat32 value) {
                csmVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) {
                    if (parameterId == curves[i].Id) {
                        curves[i].FadeOutTime = value;
                        return;
                    }
                }
            }

            csmFloat32 CubismMotion::GetParameterFadeInTime(CubismIdHandle parameterId) const {
                csmVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) {
                    if (parameterId == curves[i].Id) {
                        return curves[i].FadeInTime;
                    }
                }

                return -1;
            }

            csmFloat32 CubismMotion::GetParameterFadeOutTime(CubismIdHandle parameterId) const {
                csmVector<CubismMotionCurve> &curves = _motionData->Curves;

                for (csmInt16 i = 0; i < _motionData->CurveCount; ++i) {
                    if (parameterId == curves[i].Id) {
                        return curves[i].FadeOutTime;
                    }
                }

                return -1;
            }

            void CubismMotion::IsLoop(csmBool loop) {
                this->_isLoop = loop;
            }

            csmBool CubismMotion::IsLoop() const {
                return this->_isLoop;
            }

            void CubismMotion::IsLoopFadeIn(csmBool loopFadeIn) {
                this->_isLoopFadeIn = loopFadeIn;
            }

            csmBool CubismMotion::IsLoopFadeIn() const {
                return this->_isLoopFadeIn;
            }

            csmFloat32 CubismMotion::GetLoopDuration() {
                return _loopDurationSeconds;
            }

            void CubismMotion::SetEffectIds(const csmVector<CubismIdHandle> &eyeBlinkParameterIds,
                                            const csmVector<CubismIdHandle> &lipSyncParameterIds) {
                _eyeBlinkParameterIds = eyeBlinkParameterIds;
                _lipSyncParameterIds = lipSyncParameterIds;
            }

            const csmVector<const csmString *> &
            CubismMotion::GetFiredEvent(csmFloat32 beforeCheckTimeSeconds,
                                        csmFloat32 motionTimeSeconds) {
                _firedEventValues.UpdateSize(0);
                /// イベントの発火チェック
                for (csmInt32 u = 0; u < _motionData->EventCount; ++u) {
                    if ((_motionData->Events[u].FireTime > beforeCheckTimeSeconds) &&
                        (_motionData->Events[u].FireTime <= motionTimeSeconds)) {
                        _firedEventValues.PushBack(&_motionData->Events[u].Value);
                    }
                }

                return _firedEventValues;
            }

        }
    }
}
